import socket
import threading
import os
import sys
from collections import defaultdict
import platform
import mimetypes
import time
from pathlib import Path
import hashlib  # 导入hashlib模块
import threading
from threading import Timer
import sqlite3  # 数据库
import GV  # 引用本系统定义的全局变量

# 文件的数据结构，包括文件名和其Hash值
DictionaryFileType = {'fileName': '', 'fileType': '', 'fileHash': ''}
# fileType 1：该文件为区块文件; 2：该区块为交易文件


# 计算指定文件的MD5
def getMD5Str(fileA, intBytes=1024):
    md5Tmp = hashlib.md5()  # 创建一个md5算法对象
    with open(fileA, 'rb') as f:  # 打开一个文件，必须是'rb'模式打开
        while 1:
            data = f.read(intBytes)  # 由于是一个文件，每次只读取固定字节
            if data:  # 当读取内容不为空时对读取内容进行update
                md5Tmp.update(data)
            else:  # 当整个文件读完之后停止update
                break
    strMD5 = md5Tmp.hexdigest()  # 获取这个文件的MD5值
    return strMD5


# 获取目录下的所有文件名，并记录文件的hash值和文件类型
def FindAllFiles(StrFilePath):
    _OutFileList = []
    if os.path.isdir(StrFilePath):
        for _AFile in os.listdir(StrFilePath):
            filePathA = os.path.join(StrFilePath, _AFile)
            if not os.path.isdir(filePathA):  # 是一个文件
                fpath, strFileName = os.path.split(filePathA)
                #shotname, extension = os.path.splitext(tmpfilename)
                dictTmp = DictionaryFileType.copy()

                dictTmp['fileName'] = strFileName
                dictTmp['fileHash'] = getMD5Str(filePathA, intBytes=1024)
                if 'block' in strFileName.lower():
                    dictTmp['fileType'] = 1  # 区块文件
                else:
                    if 'tran' in strFileName.lower():
                        dictTmp['fileType'] = 2  # 交易文件
                    else:
                        continue  # 只处理区块文件和交易文件
                _OutFileList.append(dictTmp)
    return _OutFileList

# filePathDebug = r"F:\OneDrive\1-教学\区块链\4-Program\MergeV3\SwuBlockchain\block"
# GetAllBlocks(filePathDebug)


class MyException(Exception):
    print(Exception)
    pass


class Client(object):
    def __init__(self, serverhost='localhost', Version='1.11'):
        self.SERVER_HOST = serverhost
        self.SERVER_PORT = 7777
        self.Version = Version
        # self.RootPath = os.path.abspath('.')  # current dir
        # self.BlockDir = self.RootPath + '/block'  # block file directory
        self.BlockDir = GV.BlockDir
        Path(self.BlockDir).mkdir(exist_ok=True)
        print('block path:%s\n' % (self.BlockDir))

        self.UPLOAD_PORT = None
        self.shareable = True

        self.lock = threading.Lock()

    def ClientStart(self):
        keepAutoSync = False  # 是否开启自动同步模式
        self.InitLocalDB()
        # connect to server
        print('Connecting to the server %s:%s' %
              (self.SERVER_HOST, self.SERVER_PORT))
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.server.connect((self.SERVER_HOST, self.SERVER_PORT))
        except Exception:
            print('Server Not Available.')
            return

        print('Connected')
        # upload
        uploaderProcess = threading.Thread(target=self.InitUploadToPeer)
        uploaderProcess.start()
        while self.UPLOAD_PORT is None:
            # wait until upload port is initialized
            pass
        print('Listening on the upload port %s' % self.UPLOAD_PORT)

        BlockFileListFromServer = []  # 服务器上的清单
        _LocalFileList = []  # 本地文件
        # main task
        while(True):
            _ShowStr = '\n> Please input your command( like: help): '
            _inputStr = GV.FilterInput(input(_ShowStr))  # 过滤
            _CmdStr = _inputStr.split(' ')
            if _CmdStr[0].lower() == 'help':
                _ShowStr = '> list: Get file info list from server.'
                _ShowStr += '\n> add: Add all local block file info to server.'
                _ShowStr += '\n> download: Download block file from a peer.'
                _ShowStr += '\n> autosync: Sync with peers automatically.'
                _ShowStr += '\n> stop: Stop autosync.'
                _ShowStr += '\n> exit: Exit P2P client system.'
                print(_ShowStr)

            if _CmdStr[0].lower() == 'exit':
                os._exit(0)
                sys.exit(0)

            if _CmdStr[0].lower() == 'list':
                # 获取服务器上的清单
                self.CMD_UpdateFileListFromServer()

            if _CmdStr[0].lower() == 'add':
                # 将自己拥有的file清单，若还没有上传到服务器，则上传到server
                self.CMD_AddFileInfoToServer()

            if _CmdStr[0].lower() == 'download':
                # 从其他client下载没有的区块
                self.CMD_DownloadFromPeer()

            if _CmdStr[0].lower() == 'autosync':
                # 自动批量执行
                if not keepAutoSync:
                    def AutoSyncProcess():
                        while keepAutoSync:
                            self.CMD_UpdateFileListFromServer()
                            self.CMD_AddFileInfoToServer()
                            self.CMD_DownloadFromPeer()
                            time.sleep(3) #只用于调试，后边可以去掉
                        return
                    keepAutoSync = True
                    miningThread = threading.Thread(target=AutoSyncProcess)
                    miningThread.start()
                    print('start autosync.')

            if _CmdStr[0].lower() == 'stop':
                keepAutoSync = False
                print('autosync stopped.')

    def CMD_AddFileInfoToServer(self):
        # 将自己拥有的file清单，若还没有上传到服务器，则上传到server
        _LocalFileList = FindAllFiles(self.BlockDir)
        #localHostName = socket.gethostname()
        localHostIP = socket.gethostbyname(
            socket.gethostname())  # 使用IP地址
        localUploadPort = self.UPLOAD_PORT
        # 上传列表到server
        #上传之前先获取server的列表
        dbFilename = GV.ClientDbFilename
        dbConn = sqlite3.connect(dbFilename)  # 数据库连接
        c = dbConn.cursor()
        for _DictFile in _LocalFileList:
            _SqlStrSelect = "SELECT * FROM ClientUseBlockListFromServer WHERE CUBLFS_Filename='%s' AND CUBLFS_Hash='%s' AND CUBLFS_ProviderIP='%s' AND CUBLFS_ProviderPort= %s" % (
                _DictFile['fileName'], _DictFile['fileHash'], localHostIP, localUploadPort)
            c.execute(_SqlStrSelect)
            _Rows = c.fetchall()
            if len(_Rows) == 0:  # 还未上传
                if ' ' not in _DictFile['fileName']:  # 不能有空格
                    self.AaddFileInfoToServer(_DictFile)
        dbConn.close()
        _TimeStr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print("Adding file info list to server finished at %s" % _TimeStr)
        return

    def CMD_UpdateFileListFromServer(self):
        # 获取服务器上的清单
        self.GetAllFileInfoFromServer()
        #从数据库中读取显示出来
        dbFilename = GV.ClientDbFilename

        dbConn = sqlite3.connect(dbFilename)  # 数据库连接
        c = dbConn.cursor()
        _SqlStrSelect = "SELECT CUBLFS_Filename,CUBLFS_Type,CUBLFS_Hash,CUBLFS_BlockID,CUBLFS_ProviderIP,CUBLFS_ProviderPort,CUBLFS_Timestampe FROM ClientUseBlockListFromServer ORDER BY CUBLFS_Filename ASC "
        for _Row in c.execute(_SqlStrSelect):
            print("Filename: %s, FileType: %s, FileHash: %s, PeerIP: %s, PeerPort: %s\n" % (
                _Row[0], _Row[1], _Row[2], _Row[4], _Row[5]))
        dbConn.commit()
        dbConn.close()
        _TimeStr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print("Downloading and list file info list from server finished at %s" % _TimeStr)

    def CMD_DownloadFromPeer(self, ):
        # 从其他client下载没有的区块
        _LocalFileList = FindAllFiles(self.BlockDir)

        _LocalIP = socket.gethostbyname(socket.gethostname())
        _LocalPort = self.UPLOAD_PORT

        #从数据库中读取出来
        dbFilename = GV.ClientDbFilename
        dbConn = sqlite3.connect(dbFilename)  # 数据库连接
        c = dbConn.cursor()
        _SqlStrSelect = "SELECT CUBLFS_Filename,CUBLFS_Type,CUBLFS_Hash,CUBLFS_BlockID,CUBLFS_ProviderIP,CUBLFS_ProviderPort,CUBLFS_Timestampe FROM ClientUseBlockListFromServer WHERE NOT (CUBLFS_ProviderIP='%s' AND CUBLFS_ProviderPort=%s);" % (
            _LocalIP, _LocalPort)
        _FilesDownloaded = 0
        for _Row in c.execute(_SqlStrSelect):
            _DictFile = DictionaryFileType.copy()
            _DictFile['fileName'] = _Row[0]
            _DictFile['fileType'] = _Row[1]
            _DictFile['fileHash'] = _Row[2]
            if _DictFile not in _LocalFileList:
                print("Downloading, Filename: %s, FileType: %s, FileHash: %s, PeerIP: %s, PeerPort: %s\n" % (
                    _Row[0], _Row[1], _Row[2], _Row[4], _Row[5]))
                if self.DownloadFileFromPeer(
                    _Row[0], _Row[2], _Row[4], _Row[5]):  # download
                    _FilesDownloaded = _FilesDownloaded +1
        dbConn.commit()
        dbConn.close()
        _TimeStr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print("Downloading %s files from peer finished at %s" % (_FilesDownloaded,_TimeStr))
        return

    def InitLocalDB(self): #初始化本地数据库
        self.lock.acquire()
        try:
            dbFilename = GV.ClientDbFilename
            dbConn = sqlite3.connect(dbFilename)  # 数据库连接
            c = dbConn.cursor()
            _SqlStr = "DELETE FROM ClientUseBlockListFromServer"
            c.execute(_SqlStr)
            dbConn.commit()
            dbConn.close()
        finally:
            self.lock.release()

        return
  
    def InitUploadToPeer(self):
        # listen upload port
        self.uploader = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.uploader.bind(('', 0))  # 随机端口
        # self.uploader.bind(('', 11880))
        self.UPLOAD_PORT = self.uploader.getsockname()[1]
        self.uploader.listen(5)

        while self.shareable:
            requester, addr = self.uploader.accept()
            handler = threading.Thread(
                target=self.UploadHandler, args=(requester, addr))
            handler.start()
        self.uploader.close()

    def UploadHandler(self, soc, addr):
        header = soc.recv(1024).decode().splitlines()
        try:
            method = header[0].split()[0]
            fileName = header[0].split()[1]
            fileHash = header[0].split()[2]
            version = header[0].split()[-1]
            _fileFullPath = '%s/%s' % (self.BlockDir, fileName)
            if version != self.Version:
                soc.sendall(str.encode(
                    '505 P2P-CI Version Not Supported %s\n' % self.Version))
            elif not Path(_fileFullPath).is_file():
                soc.sendall(str.encode('404 Not Found %s\n' % self.Version))
            elif method == 'GET_FILE':
                header = '200 %s\n' % self.Version
                header += 'Data: %s\n' % (time.strftime(
                    "%a, %d %b %Y %H:%M:%S GMT", time.gmtime()))
                header += 'OS: %s\n' % (platform.platform())
                header += 'Last-Modified: %s\n' % (time.strftime(
                    "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(os.path.getmtime(_fileFullPath))))
                header += 'Content-Length: %s\n' % (
                    os.path.getsize(_fileFullPath))
                header += 'Content-Type: %s\n' % (
                    mimetypes.MimeTypes().guess_type(_fileFullPath)[0])
                soc.sendall(header.encode())
                # Uploading
                try:
                    print('\nUploading...')
                    send_length = 0
                    with open(_fileFullPath, 'r') as file:
                        to_send = file.read(1024)
                        while to_send:
                            send_length += len(to_send.encode())
                            soc.sendall(to_send.encode())
                            to_send = file.read(1024)
                except Exception:
                    raise MyException('Uploading Failed')
                print('Uploading Completed.')
            else:
                raise MyException('Bad Request.(%s)' % time.time())
        except Exception as e:
            print(e)
        except Exception:
            soc.sendall(str.encode('400 P2P Bad Request %s\n' % self.Version))
        finally:
            soc.close()

    def AaddFileInfoToServer(self, dictFileA):
        # 将自己有的文件登记到server上，方便其他client访问
        msg = 'ADD %s %s %s %s\n' % (
            dictFileA['fileName'], dictFileA['fileHash'], dictFileA['fileType'], self.Version)
        msg += 'IP: %s\n' % socket.gethostbyname(socket.gethostname())
        msg += 'Port: %s\n' % self.UPLOAD_PORT
        self.server.sendall(msg.encode())
        res = self.server.recv(1024).decode()
        print('Server response: \n%s' % res)

    def GetAllFileInfoFromServer(self):
        try:
            # 获取服务器上的清单
            intIndex = 0
            _Rsp = self.GetBlockFileInfoFromServer(intIndex)
            while _Rsp == 1:
                intIndex = intIndex+1
                _Rsp = self.GetBlockFileInfoFromServer(intIndex)

            self.GetTranFileInfoFromServer()
            return 0
        except Exception as e:
            print(e)

    def GetBlockFileInfoFromServer(self, BlockID):  # 客户端从服务器获取包含指定区块号的文件清单
        try:
            str1 = 'LIST_Block %s %s\n' % (BlockID, self.Version)
            self.server.sendall(str1.encode())
            res = self.server.recv(1024).decode()
            #print('Client recieved response: \n%s' % res)
            listSplit = res.strip().split('\n')
            _Response = -1
            if listSplit[0].split(' ')[0] == '200':  # 第一行， 返回200, 有数据
                for i in range(1, len(listSplit), 1):
                    fileName = listSplit[i].split(' ')[1]
                    fileType = listSplit[i].split(' ')[2]
                    fileHash = listSplit[i].split(' ')[3]
                    _BlockID = int(listSplit[i].split(' ')[4])
                    peer_host = listSplit[i].split(' ')[5]
                    peer_port = int(listSplit[i].split(' ')[6])

                    #存储到数据库中
                    self.lock.acquire()
                    try:
                        dbFilename = GV.ClientDbFilename
                        dbConn = sqlite3.connect(dbFilename)  # 数据库连接
                        c = dbConn.cursor()
                        _InsertSqlStr = "INSERT OR REPLACE INTO ClientUseBlockListFromServer(CUBLFS_Filename,CUBLFS_Type,CUBLFS_Hash,CUBLFS_BlockID,CUBLFS_ProviderIP,CUBLFS_ProviderPort,CUBLFS_Timestampe) VALUES('%s',%s,'%s',%s,'%s',%s,%s)" % (
                            fileName, fileType, fileHash, _BlockID, peer_host, peer_port, time.time())
                        c.execute(_InsertSqlStr)
                        dbConn.commit()
                        dbConn.close()
                    finally:
                        self.lock.release()
                _Response = 1
            return _Response
        except Exception as e:
            print(e)

    def GetTranFileInfoFromServer(self,):  # 客户端从服务器获取交易的文件清单
        try:
            str1 = 'LIST_Tran %s\n' % (self.Version)
            self.server.sendall(str1.encode())
            res = self.server.recv(1024).decode()
            #print('Client recieved response: \n%s' % res)
            listSplit = res.strip().split('\n')
            if listSplit[0].split(' ')[1] == '200':  # 返回200, 有数据
                for i in range(1, len(listSplit)):
                    fileName = listSplit[i].split(' ')[1]
                    fileType = listSplit[i].split(' ')[2]
                    fileHash = listSplit[i].split(' ')[3]
                    _BlockID = -1
                    peer_host = listSplit[i].split(' ')[4]
                    peer_port = int(listSplit[i].split(' ')[5])
                    #存储到数据库中
                    self.lock.acquire()
                    dbFilename = GV.ClientDbFilename
                    dbConn = sqlite3.connect(dbFilename)  # 数据库连接
                    c = dbConn.cursor()
                    _InsertSqlStr = "INSERT OR REPLACE INTO ClientUseBlockListFromServer(CUBLFS_Filename,CUBLFS_Type,CUBLFS_Hash,CUBLFS_BlockID,CUBLFS_ProviderIP,CUBLFS_ProviderPort,CUBLFS_Timestampe) VALUES('%s',%s,'%s',%s,'%s',%s,%s)" % (
                        fileName, fileType, fileHash, _BlockID, peer_host, peer_port, time.time())
                    c.execute(_InsertSqlStr)
                    dbConn.commit()
                    dbConn.close()
                    self.lock.release()
            return
        except Exception as e:
            print(e)

    def DownloadFileFromPeer(self, fileName, fileHash, peer_host, peer_port):
        _DownloadSuccess = 0 #是否下载成功
        try:
            # make connnection
            soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            # connect_ex return errors
            if soc.connect_ex((peer_host, peer_port)):
                # print('Try Local Network...')
                # if soc.connect_ex(('localhost', peer_port)):
                raise MyException('Peer Not Available')
            # make request
            msg = 'GET_FILE %s %s %s\n' % (fileName, fileHash, self.Version)
            # msg += 'Host: %s\n' % socket.gethostname()
            localHostIP = socket.gethostbyname(
                socket.gethostname())  # 使用IP地址
            msg += 'Host: %s\n' % localHostIP
            msg += 'OS: %s\n' % platform.platform()
            soc.sendall(msg.encode())

            # Downloading

            header = soc.recv(1024).decode()
            print('Recieve response header: \n%s' % header)
            header = header.splitlines()
            if header[0].split(' ')[0] == '200':
                path = '%s/%s' % (self.BlockDir, fileName)
                print('Downloading...')
                try:
                    with open(path, 'w') as file:
                        content = soc.recv(1024)
                        while content:
                            file.write(content.decode())
                            content = soc.recv(1024)
                except Exception:
                    raise MyException('Downloading Failed')

                total_length = int(header[4].split()[1])
                # print('write: %s | total: %s' % (os.path.getsize(path), total_length))

                if os.path.getsize(path) < total_length:
                    raise MyException('Downloading Failed')

                _DownloadSuccess = 1
                print('Downloading file %s completed.' % fileName)

            elif header[0].split()[0] == '400':
                # raise MyException('Invalid Input.')
                print('Invalid Input.')
            elif header[0].split()[0] == '404':
                # raise MyException('File Not Available.')
                print('File Not Available.')
            elif header[0].split()[1] == '500':
                # raise MyException('Version Not Supported.')
                print('Version Not Supported.')
        finally:
            soc.close()
            return _DownloadSuccess

    def InvalidInput(self):
        raise MyException('Invalid Input.')

    def ShutDown(self):
        print('\nShutting Down...')
        self.server.close()
        try:
            sys.exit(0)
        except SystemExit:
            os._exit(0)


reqServerAdd = input(
    '\nInput server address to connect(leave blank = 127.0.0.1):')
if len(reqServerAdd.strip()) == 0:
    reqServerAdd = '127.0.0.1'
client = Client(serverhost=reqServerAdd)
client.ClientStart()
